/*******************************************************************************
 * 
 *  Struts2-Conversation-Plugin - An Open Source Conversation- and Flow-Scope Solution for Struts2-based Applications
 *  =================================================================================================================
 * 
 *  Copyright (C) 2012 by Rees Byars
 *  http://code.google.com/p/struts2-conversation/
 * 
 * **********************************************************************************************************************
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 *  the License. You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 *  an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *  specific language governing permissions and limitations under the License.
 * 
 * **********************************************************************************************************************
 * 
 *  $Id: ConversationInterceptor.java reesbyars $
 ******************************************************************************/
package com.google.code.rees.scope.struts2;

import java.util.Locale;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.StrutsStatics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.rees.scope.container.ScopeContainer;
import com.google.code.rees.scope.container.ScopeContainerProvider;
import com.google.code.rees.scope.conversation.ConversationAdapter;
import com.google.code.rees.scope.conversation.context.ConversationContextManager;
import com.google.code.rees.scope.conversation.context.HttpConversationContextManagerProvider;
import com.google.code.rees.scope.conversation.exceptions.ConversationException;
import com.google.code.rees.scope.conversation.exceptions.ConversationIdException;
import com.google.code.rees.scope.conversation.processing.ConversationProcessor;
import com.google.code.rees.scope.conversation.processing.InjectionConversationProcessor;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.Interceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.LocalizedTextUtil;

/**
 * 
 * This interceptor uses an {@link InjectionConversationProcessor} to process conversation states and scopes.
 * 
 * @author rees.byars
 *
 */
public class ConversationInterceptor implements Interceptor {

    private static final long serialVersionUID = -72776817859403642L;
    private static final Logger LOG = LoggerFactory.getLogger(ConversationInterceptor.class);
    
    /**
     * This key can be used in a message resource bundle to specify a message in the case of a user
     * submitting a request with an invalid conversation ID (i.e. the conversation has already ended or expired)
     * and also to map results as this will be the result value
     */
    public static final String CONVERSATION_ID_EXCEPTION_KEY = "struts.conversation.invalid.id";
    
    /**
     * This key can be used in a message resource bundle to specify a message in the case of a an
     * unexpected error occurring during conversation processing and also to map results as this will 
     * be the result value.
     * <p>
     * Of course, we don't expect that this will happen ;)
     */
    public static final String CONVERSATION_EXCEPTION_KEY = "struts.conversation.general.error";
    
    /**
     * In the case of an invalid id result, this key can be used to retrieve the offending
     * conversation's name from the {@link com.opensymphony.xwork2.util.ValueStack ValueStack}.
     * This value can then be referenced in a message with the expression ${conversation.name}
     */
    public static final String CONVERSATION_EXCEPTION_NAME_STACK_KEY = "conversation.name";
    
    /**
     * In the case of an invalid id result, this key can be used to retrieve the offending
     * conversation ID from the {@link com.opensymphony.xwork2.util.ValueStack ValueStack}.
     * This value can then be referenced in a message with the expression ${conversation.id}
     */
    public static final String CONVERSATION_EXCEPTION_ID_STACK_KEY = "conversation.id";

    protected HttpConversationContextManagerProvider contextManagerProvider;
    protected ConversationProcessor processor;
    protected ScopeContainer scopeContainer;
    
    @Inject
    public void setScopeContainerProvider(ScopeContainerProvider scopeContainerProvider) {
    	scopeContainer = scopeContainerProvider.getScopeContainer();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void destroy() {
        LOG.info("Destroying the ConversationInterceptor...");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void init() {

        LOG.info("Initializing the Conversation Interceptor...");
        
        contextManagerProvider = scopeContainer.getComponent(HttpConversationContextManagerProvider.class);
        
        processor = scopeContainer.getComponent(ConversationProcessor.class);
        
        LOG.info("...Conversation Interceptor successfully initialized.");

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
    	
    	try {
    		
    		HttpServletRequest request = (HttpServletRequest) invocation.getInvocationContext().get(StrutsStatics.HTTP_REQUEST);
        	ConversationContextManager contextManager = contextManagerProvider.getManager(request);
        	final ConversationAdapter adapter = new StrutsConversationAdapter(invocation, contextManager);
    		
    		processor.processConversations(adapter);
    		
    		invocation.addPreResultListener(new PreResultListener() {
    			@Override
    			public void beforeResult(ActionInvocation invocation, String resultCode) {
    				adapter.executePostProcessors();
    				invocation.getStack().getContext().put(StrutsScopeConstants.CONVERSATION_ID_MAP_STACK_KEY, adapter.getViewContext());
    			}
            });
            
            return invocation.invoke();
    		
    	} catch (ConversationIdException cie) {
    		
    		return this.handleIdException(invocation, cie);
    		
    	} catch (ConversationException ce) {
    		
    		return this.handleUnexpectedException(invocation, ce);
    		
    	} finally {
    		
    		ConversationAdapter.cleanup();
    		
    	}
        
    }
    
    /**
     * Handles logging and UI messages for ConversationIdExceptions
     * 
     * @param invocation
     * @param cie
     * @return
     */
    protected String handleIdException(ActionInvocation invocation, ConversationIdException cie) {
    	
    	LOG.warn("ConversationIdException occurred in Conversation Processing, returning result of " + CONVERSATION_ID_EXCEPTION_KEY);
		
		Locale locale = invocation.getInvocationContext().getLocale();
		Map<String, Object> stackContext = invocation.getStack().getContext();
		
		//Placing exception details on stack for OGNL retrieval in messages
		stackContext.put(CONVERSATION_EXCEPTION_NAME_STACK_KEY, cie.getConversationName());
		stackContext.put(CONVERSATION_EXCEPTION_ID_STACK_KEY, cie.getConversationId());
		
		//message key for the conversation
		final String conversationSpecificMessageKey = CONVERSATION_ID_EXCEPTION_KEY + "." + cie.getConversationName();
		
		//First, we attempt to get a conversation-specific message from a bundle
		String errorMessage = LocalizedTextUtil.findText(this.getClass(), conversationSpecificMessageKey, locale);
		
		//If conversation specific message not found, get generic message, if that not found use default
		if (errorMessage == null || errorMessage.equals(conversationSpecificMessageKey)) {
			errorMessage = LocalizedTextUtil.findText(this.getClass(), CONVERSATION_ID_EXCEPTION_KEY, locale,
                    "The workflow that you are attempting to continue has ended or expired.  Your requested action was not processed.", new Object[0]);
		}
		
		if (LOG.isDebugEnabled()) {
			LOG.debug("Placing Conversation Error Message on stack (key={" + CONVERSATION_ID_EXCEPTION_KEY + "}):  " + errorMessage);
    	}
		
		//Placing message on stack instead of in actionErrors for retrieval in UI
		stackContext.put(CONVERSATION_ID_EXCEPTION_KEY, errorMessage);
		
		this.handleConversationErrorAware(invocation.getProxy(), errorMessage);
		
		return CONVERSATION_ID_EXCEPTION_KEY;
    }
    
    /**
     * Handles logging and UI messages for ConversationExceptions
     * 
     * @param invocation
     * @param ce
     * @return
     */
    protected String handleUnexpectedException(ActionInvocation invocation, ConversationException ce) {
    	
    	LOG.error("An unexpected exception occurred in Conversation Processing, returning result of " + CONVERSATION_EXCEPTION_KEY);
		
		Locale locale = invocation.getInvocationContext().getLocale();
		
		String errorMessage = LocalizedTextUtil.findText(this.getClass(), CONVERSATION_EXCEPTION_KEY, locale,
                    "An unexpected error occurred while processing you request.  Please try again.", new Object[0]);
		
		if (LOG.isDebugEnabled()) {
    		LOG.debug("Placing Conversation Error Message on stack (key={" + CONVERSATION_EXCEPTION_KEY + "}):  " + errorMessage);
    	}
		
		//Placing message on stack instead of in actionErrors for retrieval in UI
		invocation.getStack().getContext().put(CONVERSATION_EXCEPTION_KEY, errorMessage);
		
		this.handleConversationErrorAware(invocation.getProxy(), errorMessage);
		
		return CONVERSATION_EXCEPTION_KEY;
    }
    
    /**
	 * This provides extra functionality over placing on stack in that it allows for
	 * easily propagating the error through a redirect using a static result param:
	 * <p>
	 * <tt>&ltparam name="conversationError">${conversationError}&lt/param></tt>
	 */
    protected void handleConversationErrorAware(ActionProxy proxy, String errorMessage) {
    	
		Object action = proxy.getAction();
		
		if (action instanceof ConversationErrorAware) {
			
			LOG.debug("Action is an instance of ConversationErrorAware; setting conversation error.");
			
			((ConversationErrorAware) action).setConversationError(errorMessage);
			
		}
    }

}
